「 Kata Containers 」源码走读 — virtcontainers/device
based on 3.0.0
DeviceReceiver 是一组相对而言较底层的接口声明,其直接调用 hypervisor 执行设备热插拔等操作;而 Device 描述了设备的实现细节,内部会调用 DeviceReceiver 的接口实现各自的热插拔功能;而 DeviceManager 则对外提供设备管理能力,其内部屏蔽了设备的具体类型,而是直接调用 Device 的接口管理设备。
DeviceReceiver
src/runtime/pkg/device/api/interface.go
DeviceReceiver 的实现由 Sandbox 接口完成。
DeviceReceiver 中声明的 GetHypervisorType 为参数获取,无复杂逻辑,不作详述。
HotplugAddDevice
热添加设备到 sandbox 中
- 调用 sandboxController 的 AddDevice,将 device 的 GetHostPath 添加到 cgroup 管理中
- 如果设备类型为 vfio
- 调用 device 的 GetDeviceInfo,获取 iommu group 中所有设备
- 调用 hypervisor 的 HotplugAddDevice,热添加所有 vfio 设备
group 是 IOMMU 能够进行 DMA 隔离的最小硬件单元,一个 group 内可能只有一个 device,也可能有多个 device,这取决于物理平台上硬件的 IOMMU 拓扑结构。 设备直通的时候一个 group 里面的设备必须都直通给一个虚拟机。 不能够让一个group 里的多个 device 分别从属于 2 个不同的 VM,也不允许部分 device 在 host 上而另一部分被分配到 guest 里, 因为就这样一个 guest 中的 device 可以利用 DMA 攻击获取另外一个 guest 里的数据,就无法做到物理上的 DMA 隔离。
- 如果设备类型为 block 或 vhost-user-blk-pci,直接调用 hypervisor 的 HotplugAddDevice,热添加设备
- 如果设备类型为 generic(即非 vfio、block 或者 vhost-user 设备),则不做操作
根据注释的 TODO,猜测后续版本会有操作,截至 3.0.0 暂无逻辑 - 如果为其他设备类型,则不做操作
HotplugRemoveDevice
热移除 sandbox 中的设备
- 如果设备类型为 vfio
- 调用 device 的 GetDeviceInfo,获取 iommu group 中所有设备
- 调用 hypervisor 的 HotplugRemoveDevice,热移除所有 vfio 设备
- 如果设备类型为 block(非 PMEM 设备,因为持久内存设备无法热移除)或 vhost-user-blk-pci
- 调用 device 的 GetDeviceInfo,获取设备详情
- 调用 hypervisor 的 HotplugRemoveDevice,热移除设备
- 如果设备类型为 generic(即非 vfio、block 或者 vhost-user 设备),则不做操作
根据注释的 TODO,猜测后续版本会有操作,截至 3.0.0 暂无逻辑 - 如果为其他设备类型,则不做操作
- 调用 sandboxController 的 RemoveDevice,将 device 的 GetHostPath 从 cgroup 管理中移除
GetAndSetSandboxBlockIndex
获取并设置 virtio-block 索引,仅支持 virtio-blk 和 virtio-scsi 类型设备
用于记录分配给 sandbox 中容器的块设备索引(通过 BlockIndexMap(map[int]struct{}))
- 获取维护在 sandbox.state.BlockIndexMap 中,从 0 到 65534 范围内没有被使用的索引 ID
UnsetSandboxBlockIndex
释放记录的 virtio-block 索引,仅支持 virtio-blk 和 virtio-scsi 类型设备
用于记录分配给 sandbox 中容器的块设备索引(通过 BlockIndexMap(map[int]struct{}))
- 移除维护在 sandbox.state.BlockIndexMap(map[int]struct{})中的索引
AppendDevice
向 sandbox 中添加一个 vhost-user 类型的设备,用于向 hypervisor 传递启动参数
- 如果设备类型为 vhost-user-scsi-pci、virtio-net-pci、vhost-user-blk-pci 和 vhost-user-fs-pci
- 调用 device 的 GetDeviceInfo,获取设备信息
- 调用 hypervisor 的 AddDevice,添加设备
- 如果设备类型为 vfio
- 调用 device 的 GetDeviceInfo,获取 vfio group 中所有设备
- 调用 hypervisor 的 AddDevice,添加所有 vfio 设备
- 其余设备类型均不支持
Device
src/runtime/pkg/device/api/interface.go
Device 有以下实现方式:GenericDevice、VFIODevice、BlockDevice、VhostUserBlkDevice、VhostUserFSDevice、VhostUserNetDevice 和 VhostUserSCSIDevice,其中均以 GenericDevice 为基础,扩展部分方法。
1 | // DeviceInfo is an embedded type that contains device data common to all types of devices. |
DeviceInfo 描述了设备的属性信息,通常是根据 OCI spec 中获得,并根据具体的实际设备类型覆盖。
1 | // VFIODevice is a vfio device meant to be passed to the hypervisor |
一个 VFIO 设备也就是一组 IOMMU 设备。
1 | // BlockDevice refers to a block storage device implementation. |
1 | // VhostUserBlkDevice is a block vhost-user based device |
1 | // VhostUserFSDevice is a virtio-fs vhost-user device |
1 | // VhostUserNetDevice is a network vhost-user based device |
1 | // VhostUserSCSIDevice is a SCSI vhost-user based device |
1 | // GenericDevice refers to a device that is neither a VFIO device, block device or VhostUserDevice. |
1 | // VFIODev represents a VFIO drive used for hotplugging |
VFIODev 描述了 VFIODevice 设备特有的属性信息,也可以理解为 IOMMU 设备的信息。
1 | // BlockDrive represents a block storage drive which may be used in case the storage |
BlockDrive 描述了 BlockDevice 设备特有的属性信息,除了在 BlockDevice 中使用,SWAP 和 VM 镜像也是由 BlockDrive 构建。
1 | // VhostUserDeviceAttrs represents data shared by most vhost-user devices |
VhostUserDeviceAttrs 描述了 VhostUserBlkDevice、VhostUserFSDevice、VhostUserNetDevice 和 VhostUserSCSIDevice 设备特有的属性信息。
Device 中声明的 DeviceID、GetAttachCount、GetHostPath 和 GetMajorMinor 均为参数获取与赋值,无复杂逻辑,不作详述。
此外,DeviceType 返回各自 Device 实现的类型(如 generic、vfio、vhost-user-blk-pci、vhost-user-fs-pci、virtio-net-pci 和 vhost-user-scsi-pci);GetDeviceInfo 返回各自 Device 实现的属性信息;Reference 和 Dereference 用于维护设备的引用计数,未达到最多(^uint(0),即 2 的 64 次方减一)和最少引用时,则计数加一或减一并返回;Save 和 Load 用于 Device 和 DeviceState(结构类似,用于描述状态数据)之间转换,不同的实现额外赋值其各自的属性信息。
bumpAttachCount
记录设备的 attach 次数
bumpAttachCount 并非 Device 声明的接口,而是 GenericDevice 的一个常用方法,用于判断是否需要执行实际 attach 或 detach 操作,函数入参中的 bool 用于表明是否为 attach 操作,出参中的 bool 用于表明是否为单纯的计数。
- 如果为 attach 操作
- 如果当前 attach 计数为 0,则计数加一,并返回 false,即需要执行实际的 attach 操作
- 如果当前 attach 计数为 ^uint(0)(即 2 的 64 次方减一),则返回 true 和设备 attach 次数过多的错误
- 除此之外,默认计数加一,并返回 true,即不需要执行实际的 attach 操作
- 如果为 detach 操作
- 如果当前 attach 计数为 0,则返回 true 和设备并未 attach 的错误
- 如果当前 attach 次数为 1,则计数减一,并返回 false,即需要执行实际的 detach 操作
- 除此之外,默认计数减一,并返回 true,即不需要执行实际的 detach 操作
Attach
attach 设备
根据不同的实现,可能是冷添加或者热添加
GenericDevice
- 调用 bumpAttachCount,维护 attach 计数,不执行实际操作
VFIODevice
- 调用 bumpAttachCount,维护 attach 计数,判断是否执行后续实际操作
- 遍历 /sys/kernel/iommu_groups/<device.DeviceInfo.HostPath>/devices,获取 VFIO 设备的 BDF(PCIe 总线中的每一个功能都有一个唯一的标识符与之对应。这个标识符就是 BDF,即 Bus,Device,Function)、sysfsDev 和设备类型,判断是否为 PCIe 设备,获取 PCI class 等信息,如果为 PCIe 设备,生成 Bus 信息
具体参考 VFIODev 结构体注释 - 如果设备必须冷添加,则调用 devReceiver 的 AppendDevice,添加设备;否则调用 devReceiver 的 HotplugAddDevice,热添加设备
BlockDevice
- 调用 bumpAttachCount,维护 attach 计数,判断是否执行后续实际操作
- 调用 devReceiver 的 GetAndSetSandboxBlockIndex,设置并返回可用的索引 ID
- 根据 device.DeviceInfo.DriverOptions[“block-driver”],回写对应的字段(SCSIAddr 和 VirtPath)
- 如果未指定则视为 virtio-scsi,根据索引 ID 计算出 SCSIAddr,格式为 <index / 256>:<index % 256>
qemu 代码建议 scsi-id 可以取值从 0 到 255(含),而 lun 可以取值从 0 到 16383(含)。 但是超过 255 的 lun 值似乎不遵循一致的 SCSI 寻址。 因此限制为 255 - 如果指定不为 nvdimm,则根据索引 ID 计算出 VirtPath,例如 /dev/vda
其中,索引 0 对应 vda,25 对应 vdz,27 对应 vdab,704 对应 vdaac,18277 对应 vdzzz
- 如果未指定则视为 virtio-scsi,根据索引 ID 计算出 SCSIAddr,格式为 <index / 256>:<index % 256>
- 调用 devReceiver 的 HotplugAddDevice,热添加设备
VhostUserBlkDevice
- 调用 bumpAttachCount,维护 attach 计数,判断是否执行后续实际操作
- 根据 device.DeviceInfo.DriverOptions[“block-driver”],判断 block-driver 是否是 virtio-blk
如果未指定则视为 virtio-scsi;如果指定为 virtio-blk、virtio-blk-ccw 或 virtio-mmio 则视为 virtio-blk - 如果是 virtio-blk,则调用 devReceiver 的 GetAndSetSandboxBlockIndex,获取未被使用的块索引;否则,索引默认为 -1
- 调用 devReceiver 的 HotplugAddDevice,热添加设备
VhostUserFSDevice、VhostUserNetDevice、VhostUserSCSIDevice
VhostUserFSDevice、VhostUserNetDevice 和 VhostUserSCSIDevice 实现方式一致,以 GenericDevice 为例
- 调用 bumpAttachCount,维护 attach 计数,判断是否执行后续实际操作
- 调用 devReceiver 的 AppendDevice,添加设备
Detach
detach 设备
不同的实现下未必支持 detach 操作
GenericDevice、VhostUserFSDevice、VhostUserNetDevice、VhostUserSCSIDevice
GenericDevice、VhostUserFSDevice、VhostUserNetDevice 和 VhostUserSCSIDevice 实现方式一致,以 GenericDevice 为例
- 调用 bumpAttachCount,维护 attach 计数,不执行实际操作
VFIODevice
- 调用 bumpAttachCount,维护 attach 计数,判断是否执行后续实际操作
- 如果设备是冷添加的,说明没有运行后的 attach 动作,因此则无需 detach;否则,调用 devReceiver 的 HotplugRemoveDevice,热移除设备
BlockDevice
- 调用 bumpAttachCount,维护 attach 计数,判断是否执行后续实际操作
- 调用 devReceiver 的 HotplugRemoveDevice,热移除设备
VhostUserBlkDevice
- 调用 bumpAttachCount,维护 attach 计数,判断是否执行后续实际操作
- 调用 devReceiver 的 HotplugRemoveDevice,热移除设备
- 根据 device.DeviceInfo.DriverOptions[“block-driver”],判断 block-driver 是否是 virtio-blk。如果是 virtio-blk,则调用 devReceiver 的 UnsetSandboxBlockIndex,释放记录的 virtio-block 索引
如果未指定则视为 virtio-scsi;如果指定为 virtio-blk、virtio-blk-ccw 或 virtio-mmio 则视为 virtio-blk
DeviceManager
src/runtime/pkg/device/api/interface.go
1 | type deviceManager struct { |
Device 中声明的 IsDeviceAttached、GetDeviceByID 和 GetAllDevices 为参数获取,无复杂逻辑,不作详述。
NewDevice
初始化设备
- 如果设备不是 pmem 类型(即 devInfo.Pmem 为 false)
- 如果启用了 [hypervisor].enable_vhost_user_store、devInfo.DevType 为 b 并且设备 devInfo.Major 是 242(即 vhost-user-scsi)或者 241(即 vhost-user-blk),则获取 <vhostUserStorePath>/block/devices 目录下,格式为 major:minor 的文件名,作为 socket 文件,返回 <vhostUserStorePath>/block/sockets/<socket> 文件路径
用于获取 vhost-user 设备的主机路径。 对于 vhost-user 块设备,如 vhost-user-blk 或 vhost-user-scsi,其 socket 应位于目录 <vhostUserStorePath>/block/sockets/ 下,它对应的设备节点应该在目录 <vhostUserStorePath>/block/devices/ 下 - 如果 devInfo.DevType 为 c 或者 u,则 uevent 路径为 /sys/dev/char/<major:minor>/uevent;如果 devInfo.DevType 为 b,则 uevent 路径为 /sys/dev/block/<major:minor>/uevent。如果 uevent 文件不存在,则返回 devInfo.ContainerPath,否则读取文件内容(文件为 ini 格式),解析 DEVNAME 项,返回 /dev/<DEVNAME > 文件路径
某些设备(例如 /dev/fuse、/dev/cuse)并不总是在 /sys/dev 下实现 sysfs 接口,这些设备默认由 docker 传递。 只需返回在设备配置中传递的路径,这确实意味着这些设备不支持设备重命名 - 设置 devInfo.HostPath 为上述返回的路径
- 如果启用了 [hypervisor].enable_vhost_user_store、devInfo.DevType 为 b 并且设备 devInfo.Major 是 242(即 vhost-user-scsi)或者 241(即 vhost-user-blk),则获取 <vhostUserStorePath>/block/devices 目录下,格式为 major:minor 的文件名,作为 socket 文件,返回 <vhostUserStorePath>/block/sockets/<socket> 文件路径
- 根据 devInfo.Major 和 devInfo.Minor,判断设备是否已经存在 deviceManager 的 devices 中,存在则直接返回即可
- 为了避免 deviceID 冲突,重新生成 devInfo.ID
- 根据设备类别,初始化对应的设备
- 如果 devInfo.HostPath 为 /dev/vfio/xxx(排除 /dev/vfio/vfio 字符设备),则视为 vfio 设备类型
- 如果 devInfo.DevType 为 b,并且 devInfo.Major 为 241,则视为 vhost-user-blk 设备类型
- 如果 devInfo.DevType 为 b,则视为 block 设备类型(也就是 devInfo.Major 不为 241)
- 除此之外,均视为 generic 设备类型(也就是 vhost-user-fs、vhost-user-net 和 vhost-user-scsi 设备均为此类型)
- 调用 device 的 Reference,维护设备的引用计数
- 维护 deviceManager 中的设备信息,其中 key 为调用 device 的 DeviceID 获得,后续用于判断设备是否已经创建
RemoveDevice
移除维护的设备信息
- 校验设备是否已经创建
- 调用 device 的 Dereference,移除引用
- 如果移除后引用为 0,则并调用 device 的 GetAttachCount,校验当前设备 attach 次数是否为 0,移除维护在 deviceManager 的设备信息
AttachDevice
attach 设备
- 校验设备是否已经创建
- 调用 device 的 Attach,attach 设备
DetachDevice
detach 设备
- 校验设备是否已经创建
- 调用 device 的 GetAttachCount,校验当前设备 attach 次数是否不为 0
- 调用 device 的 Detach,detach 设备
LoadDevices
加载设备信息
- 遍历入参 []config.DeviceState 中每一个设备信息,根据其类型初始化对应的 device 对象
- 调用 device 的 Load,加载设备
- 维护 deviceManager 中的设备信息,其中 key 为调用 device 的 DeviceID 获得,后续用于判断设备是否已经创建
「 Kata Containers 」源码走读 — virtcontainers/device
http://shenxianghong.github.io/2023/03/18/2023-03-18 Kata Containers 源码走读 - virtcontainers device/